<?php
//
//	file: sys/utf8/utf8.class.php
//	author: Pierre ROGE
//	begin: 28/07/2007
//	version: 0.0.2 - 29/01/2008
//	license: http://opensource.org/licenses/gpl-license.php GNU General Public License (GPL)
//

if ( !defined('SYS') )
{
	die('Not allowed');
}

class sys_utf8 extends sys_stdclass
{
	var $data;
	var $lowers;
	var $locale;
	var $regexp_u;
	var $regexp_char;

	function __construct($api_name)
	{
		parent::__construct($api_name);
		$this->data = array();
		$this->lowers = array();
		$this->locale = false;
		$this->regexp_u = false;
		$this->regexp_char = false;
	}

	// set locale is used for strtolower/upper
	// i18n context: this one should be used to qualify strtoupper/lower
	// it can be (according to unicode 5.0) 'az' (Azerbaijani), 'tr' (Turkish), 'lt' (Lithuanian)
	function set_locale($locale=false)
	{
		$this->locale = $locale && in_array($locale, array('az', 'tr', 'lt')) ? $locale : false;
	}

	function __destruct()
	{
		unset($this->regexp_char);
		unset($this->regexp_u);
		unset($this->locale);
		unset($this->lowers);
		unset($this->data);
		parent::__destruct();
	}

	function set()
	{
		$sys = &$GLOBALS[SYS];

		// force php to work with utf-8
		ini_set('default_charset', 'UTF-8');

		// limit strtolower/upper() to ascii chars only
		setlocale(LC_CTYPE, 'C');

		// /u should be present since php 4.1.0 for *NIX, and 4.2.3 for w32/64 servers
		$this->regexp_u = @preg_match('#.#u', 'a') ? 'u' : '';
		$this->regexp_char = $this->regexp_u ? '.' : '(?:[^\x80-\xbf][\x80-\xbf]*)';
	}

	// static: guess the appropriate class & load the layer
	function get_layer()
	{
		$sys = &$GLOBALS[SYS];
		$ini = array(
			'native' => array('file' => 'utf8.native.class', 'class' => 'sys_utf8_native'),
			'mbstring' => array('file' => 'utf8.mbstring.class', 'class' => 'sys_utf8_mbstring'),
			'wrapper' => array('file' => 'utf8.wrapper.class', 'class' => 'sys_utf8_wrapper'),
		);
		// with PHP 6, unicode is nativaly supported
		// nb.: due to lack of information, we disable it for now
		$mode = false && version_compare(PHP_VERSION, '6.0.0', '>=') ? 'native' : (extension_loaded('mbstring') ? 'mbstring' : 'wrapper');
		if ( !class_exists($ini[$mode]['class']) )
		{
			include($sys->path . 'utf8/' . $ini[$mode]['file'] . $sys->ext);
		}
		return $ini[$mode]['class'];
	}

	// private: load an unicode 5.0 chart
	function load_chart($chart)
	{
		$sys = &$GLOBALS[SYS];
		if ( !isset($this->data[$chart]) )
		{
			$this->data[$chart] = array();
			if ( !($file = $sys->path . 'utf8/charts/' . $chart . $sys->ext) || !file_exists($file) )
			{
				trigger_error($chart . ' is missing.', E_USER_ERROR);
			}
			include($file);
			if ( !isset($this->lowers[$chart]) )
			{
				$this->lowers[$chart] = 0;
			}
		}
		return !empty($this->data[$chart]);
	}

	// free memory from charts
	function free()
	{
		$this->data = array();
	}

	//
	// covered by the layers
	//

	function strlen($str) {}
	function substr($str, $start, $length=null) {}

	//
	// some wrappers
	//

	function strtoupper($str)
	{
		return $this->case_convert($str, 'uppers');
	}

	function strtolower($str)
	{
		return $this->case_convert($str, 'lowers');
	}

	//
	// more advanced methods
	//

	// sanatize a utf-8 string (strip \r, replace all not valid chars with the utf-8 replacement char (0xfffd)
	// based on http://www.w3.org/International/questions/qa-forms-utf-8.en.php
	// nb.: the native php6 or ICU will probably do the normalization nativaly, what will require to move this to the layers
	function normalize($str)
	{
		// strip \r, remove BOM
		if ( !empty($str) )
		{
			$str = str_replace(array("\r\n", "\r", "\xef\xbf\xbe"), array("\n", "\n", ''), $str);
		}
		// normalize the string NFC if not already
		if ( !$this->is_nfc($str) )
		{
			$this->nfc($str);
		}
		return $str;
	}

	// returned a string identifier: case folded, cleaned up from comfusable chars, NFKC
	// designed to be used as a varchar key with data coming from the user (group name, username, etc.)
	// confusable detection: see http://www.unicode.org/reports/tr39/#Confusable_Detection
	function get_identifier($str)
	{
		if ( !empty($str) )
		{
			// remove case distinction
			$this->case_folding($str);

			// remove confusable
			if ( $this->load_chart('confusables') )
			{
				// normalize the string KD if not pure ASCII
				if ( preg_match('#[\x80-\xff]#', $str) )
				{
					$this->nfkd($str);
				}
				$str = strtr($str, $this->data['confusables']);
			}
			// remove double spaces
			$str = preg_replace('#\s+#', ' ', $str);

			// normalize the string KC if not pure ASCII
			if ( preg_match('#[\x80-\xff]#', $str) )
			{
				$this->nfkc($str);
			}
		}
		return $str;
	}

	//
	// normalization
	//

	// turn str into its nfc form
	function nfc(&$str)
	{
		if ( !empty($str) )
		{
			$this->decompose_sort($str, 'normal.nfd'); // nfd()
			$this->recompose($str);
		}
	}

	// turn str into its nfd form
	function nfd(&$str)
	{
		if ( !empty($str) )
		{
			$this->decompose_sort($str, 'normal.nfd');
		}
	}

	// turn str into its nfkc form
	function nfkc(&$str)
	{
		if ( !empty($str) )
		{
			$this->decompose_sort($str, 'normal.nfkd'); // nfkd()
			$this->recompose($str);
		}
	}

	// turn str into its nfkd form
	function nfkd(&$str)
	{
		if ( !empty($str) )
		{
			$this->decompose_sort($str, 'normal.nfkd');
		}
	}

	//
	// casing
	//

	// case folding according to http://www.unicode.org/versions/Unicode5.0.0/ch05.pdf#G21180
	// str: we expect it to be NFC
	// type: simple: simple case folding, else full case folding
	// locale: if 'tr' (Turkish), use the T type (the two locale alternate for I), what we will probably never do
	// return a no more normalized string, but caseless matching type
	function case_folding(&$str, $type=false)
	{
		if ( !empty($str) )
		{
			$chart = 'case.fold' . ($type == 'simple' ? '_s' : '_f');
			if ( !isset($this->data[$chart]) && $this->load_chart('case.fold_c') && $this->load_chart($chart) )
			{
				$this->data[$chart] += $this->data['case.fold_c'];
				unset($this->data['case.fold_c']);
				unset($this->lowers['case.fold_c']);
			}
			if ( !empty($this->data[$chart]) )
			{
				if ( ($this->locale == 'tr') && $this->load_chart('case.fold_t') )
				{
					$str = strtr($str, $this->data['case.fold_t']);
				}
				$str = strtr($str, $this->data[$chart]);
			}
		}
	}

	// convert a string case in utf-8, make it NFC()
	// mode is 'uppers', 'lowers' or 'titles'
	// locale: according to unicode 5.0: 'az' (Azerbaijani), 'tr' (Turkish), 'lt' (Lithuanian) or false
	// nb.: titles does not take care of word boundaries: it will convert all chars of str if possible
	function case_convert(&$str, $mode)
	{
		if ( !empty($str) && in_array($mode, array('lowers', 'uppers', 'titles')) )
		{
			// we need to do a more advanced process: first normalize NFD (it is supposed to be NFC for now)
			$this->nfd($str);

			// check special casing
			$this->special_casing($str, $mode);

			// do the conversion with the simple case
			if ( $this->load_chart('case.' . $mode) )
			{
				$str = strtr($str, $this->data['case.' . $mode]);
			}

			// let's re-normalize the string to NFC if not pure ASCII
			if ( preg_match('#[\x80-\xff]#', $str) )
			{
				$this->nfc($str);
			}
		}
		return $str;
	}

	//
	// private: normalization
	//

	// check if a char is nfc. "maybe" will result in false
	function is_nfc(&$str)
	{
		// if empty or pure ASCII, nothing to do
		if ( empty($str) || !preg_match('#[^\x09\x0a\x0d\x20-\x7f]#', $str) )
		{
			return true;
		}
		$done_comb = $done_nfc_qc = false;

		$last_class = 0;
		$i = 0;
		while ( isset($str[$i]) )
		{
			// get the char
			$c0 = ord($str[$i]);
			$error = false;
			$k = $i + 1;
			if ( $c0 < 0xc0 )
			{
				if ( ($c0 > 0x7e) || (($c0 < 0x20) && ($c0 != 0x09) && ($c0 != 0x0a) && ($c0 != 0x0d)) )
				{
					return false;
				}
				$c = $str[$i++];
			}
			else if ( $c0 < 0xc2 )
			{
				return false;
			}
			else if ( $c0 < 0xe0 )
			{
				if ( !isset($str[$k]) || !($c1 = ord($str[$k++])) || ($c1 < 0x80) || ($c1 > 0xbf) )
				{
					return false;
				}
				$c = $str[$i++] . $str[$i++];
			}
			else if ( $c0 < 0xf0 )
			{
				if ( !isset($str[$k]) || !($c1 = ord($str[$k++])) || ($c1 < 0x80) || ($c1 > 0xbf) ||
					!isset($str[$k]) || !($c2 = ord($str[$k++])) || ($c2 < 0x80) || ($c2 > 0xbf) ||
					(($c0 == 0xe0) && ($c1 < 0xa0)) || (($c0 == 0xed) && ($c1 > 0x9f))
				)
				{
					return false;
				}
				$c = $str[$i++] . $str[$i++] . $str[$i++];
			}
			else if ( $c0 < 0xf4 )
			{
				if ( !isset($str[$k]) || !($c1 = ord($str[$k++])) || ($c1 < 0x80) || ($c1 > 0xbf) ||
					!isset($str[$k]) || !($c2 = ord($str[$k++])) || ($c2 < 0x80) || ($c2 > 0xbf) ||
					!isset($str[$k]) || !($c3 = ord($str[$k++])) || ($c3 < 0x80) || ($c3 > 0xbf) ||
					(($c0 == 0xf0) && ($c1 < 0x90)) || (($c0 == 0xf4) && ($c1 > 0x8f))
				)
				{
					return false;
				}
				$c = $str[$i++] . $str[$i++] . $str[$i++] . $str[$i++];
			}
			else
			{
				return false;
			}

			// if in combiner char & not ordered, return false
			if ( !$done_comb && ($done_comb = true) )
			{
				$this->load_chart('normal.comb_class');
			}
			if ( ($class = ($c0 >= $this->lowers['normal.comb_class']) && isset($this->data['normal.comb_class'][$c]) ? $this->data['normal.comb_class'][$c] : 0) && ($last_class > $class) )
			{
				return false;
			}
			$last_class = $class;

			// if in nfc quick check chart (no or maybe nfc), return false
			if ( !$done_nfc_qc && ($done_nfc_qc = true) )
			{
				$this->load_chart('normal.nfc_qc');
			}
			if ( ($c0 >= $this->lowers['normal.nfc_qc']) && isset($this->data['normal.nfc_qc'][$c]) )
			{
				return false;
			}
		}
		return true;
	}

	// decompose and sort a string depending the chart: nfd/nfkd
	function decompose_sort(&$str, $chart)
	{
		$hangul_sbase_uni = 0xac00;
		$hangul_sbase = "\xea\xb0\x80"; // 0xac00
		$hangul_slast = "\xed\x9e\xa3"; // 0xd7a3: SBase + (SCount = LCount * NCount)
		$hangul_tcount = 28;
		$hangul_ncount = 588; // VCount * TCount

		$this->load_chart($chart);
		$this->load_chart('normal.comb_class');

		$combiners = array();
		$res = '';
		$i = 0;
		while ( isset($str[$i]) )
		{
			// get the char and verify the utf-8 conformance
			$c0 = ord($str[$i]);
			$error = false;
			$k = $i + 1;
			if ( $c0 < 0xc0 )
			{
				$c_len = 1;
				if ( !($error = ($c0 > 0x7e) || (($c0 < 0x20) && ($c0 != 0x09) && ($c0 != 0x0a) && ($c0 != 0x0d))) )
				{
					$c = $str[$i++];
				}
			}
			else if ( $c0 < 0xc2 )
			{
				$c_len = 2;
				$error = true;
			}
			else if ( $c0 < 0xe0 )
			{
				$c_len = 2;
				if ( !($error = !isset($str[$k]) || !($c1 = ord($str[$k++])) || ($c1 < 0x80) || ($c1 > 0xbf)) )
				{
					$c = $str[$i++] . $str[$i++];
				}
			}
			else if ( $c0 < 0xf0 )
			{
				$c_len = 3;
				if ( !($error =
					!isset($str[$k]) || !($c1 = ord($str[$k++])) || ($c1 < 0x80) || ($c1 > 0xbf) ||
					!isset($str[$k]) || !($c2 = ord($str[$k++])) || ($c2 < 0x80) || ($c2 > 0xbf) ||
					(($c0 == 0xe0) && ($c1 < 0xa0)) || (($c0 == 0xed) && ($c1 > 0x9f))
				) )
				{
					$c = $str[$i++] . $str[$i++] . $str[$i++];
				}
			}
			else if ( $c0 < 0xf4 )
			{
				$c_len = 4;
				if ( !($error =
					!isset($str[$k]) || !($c1 = ord($str[$k++])) || ($c1 < 0x80) || ($c1 > 0xbf) ||
					!isset($str[$k]) || !($c2 = ord($str[$k++])) || ($c2 < 0x80) || ($c2 > 0xbf) ||
					!isset($str[$k]) || !($c3 = ord($str[$k++])) || ($c3 < 0x80) || ($c3 > 0xbf) ||
					(($c0 == 0xf0) && ($c1 < 0x90)) || (($c0 == 0xf4) && ($c1 > 0x8f))
				) )
				{
					$c = $str[$i++] . $str[$i++] . $str[$i++] . $str[$i++];
				}
			}
			else
			{
				$error = true;
				$c_len = $c0 < 0xf8 ? 4 : ($c0 < 0xfc ? 5 : ($c0 < 0xfe ? 6 : 1));
			}
			if ( $error )
			{
				$i += $c_len;
				$c_len = 3;
				$c = "\xef\xbf\xbd"; // fffd
			}

			// from there c is a valid utf-8 char
			if ( $c_len > 1 )
			{
				// we have the decomposition
				if ( ($c0 >= $this->lowers[$chart]) && isset($this->data[$chart][$c]) )
				{
					$c = $this->data[$chart][$c];
				}
				// hangul decomposition: SBase starts with 0xea & SBase + SLast starts with 0xed
				else if ( ($c0 >= 0xea) && ($c0 <= 0xed) && ($c >= $hangul_sbase) && ($c <= $hangul_slast) )
				{
					// to codepoint then to index
					$index = ((($c0 & 0x0f) << 12) | (($c1 & 0x3f) << 6) | ($c2 & 0x3f)) - $hangul_sbase_uni;

					// LVT
					if ( ($t = $index % $hangul_tcount) )
					{
						if ( $t < 25 )
						{
							$c = "\xe1\x84\xff\xe1\x85\xff\xe1\x86\xff";
							$c[8] = chr(0xa7 + $t);
						}
						else
						{
							$c = "\xe1\x84\xff\xe1\x85\xff\xe1\x87\xff";
							$c[8] = chr(0x67 + $t); // 0x80 - 25
						}
					}
					// LV
					else
					{
						$c = "\xe1\x84\xff\xe1\x85\xff";
					}
					$c[5] = chr(0xa1 + intval(($index % $hangul_ncount) / $hangul_tcount));
					$c[2] = chr(0x80 + intval($index / $hangul_ncount));
				}
			}

			// sort the char
			if ( $c_len > 1 )
			{
				$j = 0;
				while ( isset($c[$j]) )
				{
					// get the char
					$cc0 = ord($c[$j]);
					if ( $cc0 < 0x80 )
					{
						$cc_len = 1;
						$cc = $c[$j++];
					}
					else if ( $cc0 < 0xe0 )
					{
						$cc_len = 2;
						$cc = $c[$j++] . $c[$j++];
					}
					else if ( $cc0 < 0xf0 )
					{
						$cc_len = 3;
						$cc = $c[$j++] . $c[$j++] . $c[$j++];
					}
					else
					{
						$cc_len = 4;
						$cc = $c[$j++] . $c[$j++] . $c[$j++] . $c[$j++];
					}

					// combining char
					if ( ($cc_len > 1) && ($cc0 >= $this->lowers['normal.comb_class']) && isset($this->data['normal.comb_class'][$cc]) )
					{
						$class = $this->data['normal.comb_class'][$cc];
						$combiners[$class] = isset($combiners[$class]) ? $combiners[$class] . $cc : $cc;
					}
					// not a combiner
					else
					{
						if ( !empty($combiners) )
						{
							ksort($combiners);
							$res .= implode('', $combiners);
							$combiners = array();
						}
						$res .= $cc;
					}
				}
			}

			// deal with one digit char
			if ( $c_len == 1 )
			{
				if ( !empty($combiners) )
				{
					ksort($combiners);
					$res .= implode('', $combiners);
					$combiners = array();
				}
				$res .= $c;
			}
		}
		if ( !empty($combiners) )
		{
			ksort($combiners);
			$res .= implode('', $combiners);
		}
		$str = $res;
	}

	function recompose(&$str)
	{
		$hangul_sbase_uni = 0xac00;
		$hangul_sbase = "\xea\xb0\x80"; // 0xac00
		$hangul_slast = "\xed\x9e\xa3"; // 0xd7a3: SBase + (SCount = LCount * NCount)
		$hangul_tcount = 28;
		$hangul_ncount = 588; // VCount * TCount
		$hangul_lbase = "\xe1\x84\x80"; // 0x1100
		$hangul_llast = "\xe1\x84\x92"; // 0x1112: LBase + LCount - 1 (19)
		$hangul_vbase = "\xe1\x85\xa1"; // 0x1161
		$hangul_vlast = "\xe1\x85\xb5"; // 0x1175: VBase + VCount - 1
		$hangul_tbase = "\xe1\x86\xa7"; // 0x11a7
		$hangul_tlast = "\xe1\x87\x82"; // 0x11c2: TBase + TCount - 1

		$this->load_chart('normal.comb_class');
		$this->load_chart('normal.nfc');

		$last_class = -1;
		$last_hangul = false;
		$start_char = '';
		$combining = '';

		$res = '';
		$i = 0;
		while ( isset($str[$i]) )
		{
			// get the char
			$c0 = ord($str[$i]);
			if ( $c0 < 0x80 )
			{
				$c_len = 1;
				$c = $str[$i++];
			}
			else if ( $c0 < 0xe0 )
			{
				$c_len = 2;
				$c = $str[$i++] . $str[$i++];
			}
			else if ( $c0 < 0xf0 )
			{
				$c_len = 3;
				$c = $str[$i++] . $str[$i++] . $str[$i++];
			}
			else
			{
				$c_len = 4;
				$c = $str[$i++] . $str[$i++] . $str[$i++] . $str[$i++];
			}

			// ascii
			if ( $c_len == 1 )
			{
				$res .= $start_char . $combining;
				$start_char = $c;
				$combining = '';
				$last_class = 0;
				continue;
			}

			// combining char
			$pair = $start_char . $c;
			if ( ($c0 >= $this->lowers['normal.comb_class']) && isset($this->data['normal.comb_class'][$c]) )
			{
				$class = $this->data['normal.comb_class'][$c];
				if ( ($start_char !== '') && ($class > 0) && ($last_class < $class) && (ord($start_char[0]) >= $this->lowers['normal.nfc']) && isset($this->data['normal.nfc'][$pair]) )
				{
					$start_char = $this->data['normal.nfc'][$pair];
					$class = 0;
				}
				else
				{
					$combining .= $c;
				}
				$last_class = $class;
				$last_hangul = false;
				continue;
			}

			// start char
			if ( $last_class == 0 )
			{
				// in normal form
				if ( (ord($start_char[0]) >= $this->lowers['normal.nfc']) && isset($this->data['normal.nfc'][$pair]) )
				{
					$start_char = $this->data['normal.nfc'][$pair];
					$last_hangul = false;
					continue;
				}

				// maybe an hangul ? if v | t, start with 0xe1
				if ( $c0 == 0xe1 )
				{
					// case 1: start=l, c=v
					if ( ($c >= $hangul_vbase) && ($c <= $hangul_vlast) && ($start_char >= $hangul_lbase) && ($start_char <= $hangul_llast) )
					{
						$l = ord($start_char[2]) - 0x80;
						$v = ord($c[2]) - 0xa1;
						$cp = $hangul_sbase_uni + $hangul_ncount * $l + $hangul_tcount * $v;

						// hardcode the limited-range UTF-8 conversion:
						$start_char = chr(0xe0 | (($cp >> 12) & 0x0f)) . chr(0x80 | (($cp >> 6) & 0x3f)) . chr(0x80 | ($cp & 0x3f));
						$last_hangul = false;
						continue;
					}
					// case 2: start=lv (so on its final range, SBase), c=t
					if ( !$last_hangul && ($c >= $hangul_tbase) && ($c <= $hangul_tlast) && ($start_char >= $hangul_sbase) && ($start_char <= $hangul_slast) )
					{
						$t = ord($c[2]);
						$t -= $t < 0xa7 ? 0x67 : 0xa7;
						$s2 = ord($start_char[2]) + $t;
						if ( $s2 > 0xbf )
						{
							$s2 -= 0x40;
							$s1 = ord($start_char[1]) + 1;
							if ( $s1 > 0xbf )
							{
								$s1 -= 0x40;
								$s0 = ord($start_char[0]) + 1;
								$start_char[0] = chr($s0);
							}
							$start_char[1] = chr($s1);
						}
						$start_char[2] = chr($s2);

						// if there's another jamo char after this, *don't* try to merge it.
						$last_hangul = true;
						continue;
					}
				}
			}
			// end of class
			$res .= $start_char . $combining;
			$start_char = $c;
			$combining = '';
			$last_class = 0;
			$last_hangul = false;
		}
		$res .= $start_char . $combining;
		$str = $res;
	}

	//
	// private: casing
	//

	// special casing according to http://www.unicode.org/versions/Unicode5.0.0/ch03.pdf#G21130
	function special_casing(&$str, $mode)
	{
		$chart = 'case.' . $mode . '.s';
		if ( $this->load_chart($chart) )
		{
			if ( !empty($this->data[$chart]['*']) )
			{
				foreach ( $this->data[$chart]['*'] as $check_type => $letters )
				{
					$checks[$check_type] = $letters;
				}
			}
			if ( $this->locale && isset($this->data[$chart][$this->locale]) && !empty($this->data[$chart][$this->locale]) )
			{
				foreach ( $this->data[$chart][$this->locale] as $check_type => $letters )
				{
					$checks[$check_type] = $letters;
				}
			}
		}
		if ( !empty($checks) )
		{
			foreach ( $checks as $check_type => $letters )
			{
				if ( !empty($letters) )
				{
					if ( ($negate = substr($check_type, 0, 4) == 'Not_') )
					{
						$check_type = substr($check_type, 4);
					}
					if ( $check_type == '*' )
					{
						$str = strtr($str, $letters);
					}
					else if ( ($check_type = 'cc_' . strtolower($check_type)) && method_exists($this, $check_type) )
					{
						$this->$check_type($str, $letters, $negate);
					}
				}
			}
		}
	}

	// get the char at offset and increment offset
	function cc_next_char(&$str, &$offset)
	{
		return !($c0 = ord($str[$offset])) || ($c0 < 0xc0) ? $str[$offset++] : (
			$c0 < 0xe0 ? $str[$offset++] . $str[$offset++] : (
			$c0 < 0xf0 ? $str[$offset++] . $str[$offset++] . $str[$offset++] : (
			$str[$offset++] . $str[$offset++] . $str[$offset++] . $str[$offset++]
		)));
	}

	// get the char at offset and decrement offset
	function cc_previous_char(&$str, &$offset)
	{
		$c = '';
		$found = false;
		while ( !$found && isset($str[$offset]) )
		{
			$c = $str[$offset] . $c;
			$n = ord($str[$offset--]);
			$found = ($n < 0x80) || ($n > 0xbf);
		}
		return $found ? $c : false;
	}

	function cc_chrlen($str)
	{
		$sizes = array(0xc0 => 2, 0xd0 => 2, 0xe0 => 3, 0xf0 => 4);
		$i = 0;
		while ( isset($str[$i]) )
		{
			$c0 = ord($str[$i]) & 0xf0;
			$c_len = !isset($sizes[$c0]) ? 1 : $sizes[$c0];
			$i += $c_len;
		}
		return $i;
	}

	// conditional casing: final sigma case
	// \p{cased}(\p{case_ignorable})*([Char])!((\p{case-ignorable})*\p{cased})
	function cc_final_sigma(&$str, &$letters, $negate)
	{
		foreach ( $letters as $src => $dst )
		{
			// check if we have the src letter, else next letter
			if ( ($i = strpos($str, $src)) === false )
			{
				continue;
			}

			// check if we do the replacement, and where
			$offsets = array();
			$len_src = $this->cc_chrlen($src);
			while ( $i !== false )
			{
				// look forward: we must fail the test: (case ignorable)*cased
				$match = $this->cc_match_cased($str, $i + $len_src, false);
				if ( (!$match && !$negate) || ($match && $negate) )
				{
					// still candidate: look backward: we must pass the test: cased(case ignorable)*
					$match = $this->cc_match_cased($str, $i - 1, true);

					// catch the offset depending the match and negate status
					if ( ($match && !$negate) || (!$match && $negate) )
					{
						$offsets[] = $i;
					}
				}

				// search next candidate
				$i = strpos($str, $src, $i + $len_src);
			}

			// do the replacement
			$this->cc_chr_replace($str, $offsets, $len_src, $dst);
		}
	}

	// conditional casing: match cased(case ignorable)* sequences
	// forward: \p{case-ignorable}*\p{cased}
	// backward: \p{cased}\p{case_ignorable}*
	function cc_match_cased(&$str, $j, $backward)
	{
		if ( !isset($str[$j]) )
		{
			return false;
		}
		if ( !isset($this->data['case.id_ignore']) )
		{
			$this->load_chart('case.id_ignore');
		}

		// jump over case ignored sequences
		$c = false;
		while ( isset($str[$j]) )
		{
			$c = $backward ? $this->cc_previous_char($str, $j) : $this->cc_next_char($str, $j);
			if ( ($c === false) || !isset($this->data['case.id_ignore'][$c]) )
			{
				break;
			}
		}
		if ( $c === false )
		{
			return false;
		}

		// check if the char is cased type
		if ( !isset($this->data['case.id_cased']) )
		{
			$this->load_chart('case.id_cased');
		}
		return isset($this->data['case.id_cased'][$c]);
	}

	// conditional casing: more above
	// ([Char])[^\p{ccc=0}]*[\p{ccc=230}]
	function cc_more_above(&$str, &$letters, $negate)
	{
		foreach ( $letters as $src => $dst )
		{
			$this->cc_match_replace($str, $src, $dst, $negate, false, false, false, true);
		}
	}

	// conditional casing: before dot
	// ([Char])([^\p{ccc=230}\p{ccc=0}])*[\u0307] (cc.87)
	function cc_before_dot(&$str, &$letters, $negate)
	{
		$done_comb_class = false;
		foreach ( $letters as $src => $dst )
		{
			$this->cc_match_replace($str, $src, $dst, $negate, false, false, "\xcc\x87");
		}
	}

	// conditional casing: after soft dotted
	// [\p{Soft_Dotted}]([^\p{ccc=230}\p{ccc=0}])*([Char])
	function cc_after_soft_dotted(&$str, &$letters, $negate)
	{
		foreach ( $letters as $src => $dst )
		{
			$this->cc_match_replace($str, $src, $dst, $negate, true, 'case.id_soft_dotted');
		}
	}

	// conditional case: after I
	// [I]([^\p{ccc=230}\p{ccc=0}])*([Char])
	function cc_after_i(&$str, &$letters, $negate)
	{
		foreach ( $letters as $src => $dst )
		{
			$this->cc_match_replace($str, $src, $dst, $negate, false, false, 'I');
		}
	}

	// match a sequence against
	// - forward: [$chr|c in chart][^\p{ccc=230}\p{ccc=0}]*
	// - backward: [^\p{ccc=230}\p{ccc=0}]*[$chr|c in chart]
	// - match_230: forward: [$chr|c in chart][^\p{ccc=0}]*[\p{ccc=230}]
	// - match 230: backward: [^\p{ccc=0}]*[\p{ccc=230}][$chr|c in chart]
	function cc_match_replace(&$str, $src, $dst, $negate, $backward, $chart_name=false, $chr=false, $match_230=false)
	{
		// check if we have the src letter, else next letter
		if ( ($i = strpos($str, $src)) === false )
		{
			return false;
		}

		// check if we do the replacement, and where
		$offsets = array();
		$len_src = $this->cc_chrlen($src);
		while ( $i !== false )
		{
			$next_chr = $i + $len_src;
			$offset = $backward ? $i - 1 : $next_chr;
			$match = false;
			while ( isset($str[$offset]) )
			{
				// get the char
				if ( ($c = $backward ? $this->cc_previous_char($str, $offset) : $this->cc_next_char($str, $offset)) === false )
				{
					break;
				}

				// if chr granted, test first the char
				if ( ($chr !== false) && ($c == $chr ) )
				{
					$match = true;
					break;
				}

				// maybe it is ccc=0/230
				if ( isset($this->data['normal.comb_class']) || $this->load_chart('normal.comb_class') )
				{
					if ( !isset($this->data['normal.comb_class'][$c]) )
					{
						$match = false;
						break;
					}
					else if ( $this->data['normal.comb_class'][$c] == 230 )
					{
						$match = $match_230;
						break;
					}
				}

				// if not chr granted but chart, test chart
				if ( ($chart_name !== false) && (isset($this->data[$chart_name]) || $this->load_chart($chart_name)) && isset($this->data[$chart_name][$c]) )
				{
					$match = true;
					break;
				}
			}

			// catch the offset depending the match and negate status
			if ( ($match && !$negate) || (!$match && $negate) )
			{
				$offsets[] = $i;
			}

			// next candidate
			$i = strpos($str, $src, $next_chr);
		}

		// do the replacement
		return $this->cc_chr_replace($str, $offsets, $len_src, $dst);
	}

	// replace in $str at $offsets chars having len = $len_src with $dst
	function cc_chr_replace(&$str, &$offsets, $len_src, $dst)
	{
		if ( ($count_offsets = count($offsets)) )
		{
			$len_dst = $this->cc_chrlen($dst);
			if ( $len_src == $len_dst )
			{
				for ( $i = 0; $i < $count_offsets; $i++ )
				{
					for ( $j = 0; $j < $len_dst; $j++ )
					{
						$str[ $offsets[$i] + $j ] = $dst[$j];
					}
				}
			}
			else
			{
				$res = '';
				$prev_pos = 0;
				for ( $i = 0; $i < $count_offsets; $i++ )
				{
					$res .= substr($str, $prev_pos, $offsets[$i]) . $dst;
					$prev_pos = $offsets[$i] + $len_src;
				}
				$res .= substr($str, $prev_pos);
				$str = $res;
			}
			return true;
		}
		return false;
	}
}

?>